{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0748f94a",
   "metadata": {},
   "source": [
    "## ⚖️ Quick Decision Guide: LangChain vs LangGraph\n",
    "\n",
    "### 🔗 Use **LangChain** when:\n",
    "- You know the exact sequence of steps needed and tasks are executed in a specific order  \n",
    "- Building simple chatbots, Q&A systems, or RAG pipelines  \n",
    "- You need quick prototyping  \n",
    "- Your workflow is linear like:  \n",
    "```\n",
    "\n",
    "Load PDF → chunk → embed → retrieve → answer\n",
    "\n",
    "```\n",
    "\n",
    "---\n",
    "\n",
    "### 📈 Use **LangGraph** when:\n",
    "- You need stateful, multi-agent systems with complex, nonlinear workflows that involve loops and revisiting previous states  \n",
    "- Your logic must adapt, e.g., ask clarifying questions if confidence is low, retry on failures, or pause for human input  \n",
    "- Building autonomous agents that make decisions  \n",
    "- You need to maintain context over long interactions  \n",
    "\n",
    "\n",
    "---\n",
    "📢 Discover more Agentic AI notebooks on my [GitHub repository](https://github.com/lisekarimi/agentverse) and explore additional AI projects on my [portfolio](https://lisekarimi.com)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c571b450",
   "metadata": {},
   "source": [
    "## 💻 Code Examples"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b410eb8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from typing import TypedDict, Literal\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "from IPython.display import Image, display\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ab039b87",
   "metadata": {},
   "source": [
    "### 💬 Example 1: Simple Q&A System"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4092a192",
   "metadata": {},
   "source": [
    "#### 🔗 LangChain Version (Best for this use case)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af1f90db",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Simple linear chain: prompt → LLM → parse output\n",
    "llm = ChatOpenAI(model=\"gpt-4\")\n",
    "prompt = ChatPromptTemplate.from_template(\"Answer this question: {question}\")\n",
    "output_parser = StrOutputParser()\n",
    "\n",
    "# Create a chain using LangChain Expression Language (LCEL)\n",
    "chain = prompt | llm | output_parser\n",
    "\n",
    "# Execute\n",
    "result = chain.invoke({\"question\": \"What is the capital of France?\"})\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "523725a4",
   "metadata": {},
   "source": [
    "Why LangChain? This is a straightforward, one-directional flow. No loops, no complex state, just input → process → output."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11229c71",
   "metadata": {},
   "source": [
    "#### 📈 LangGraph Version (Overkill for this, but here's how it looks)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c40e57b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "class State(TypedDict):\n",
    "    question: str\n",
    "    answer: str\n",
    "\n",
    "def ask_llm(state: State) -> State:\n",
    "    # Call LLM here\n",
    "    state[\"answer\"] = \"Paris\"  # Simplified\n",
    "    return state\n",
    "\n",
    "# Build graph\n",
    "graph = StateGraph(State)\n",
    "graph.add_node(\"ask_llm\", ask_llm)\n",
    "graph.add_edge(START, \"ask_llm\")\n",
    "graph.add_edge(\"ask_llm\", END)\n",
    "\n",
    "app = graph.compile()\n",
    "result = app.invoke({\"question\": \"What is the capital of France?\"})\n",
    "print(result[\"answer\"])\n",
    "\n",
    "# Generate and display the graph\n",
    "graph_image = app.get_graph().draw_mermaid_png()\n",
    "display(Image(graph_image))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f7fc295",
   "metadata": {},
   "source": [
    "### 🎯 Example 2: Customer Support Agent with Decision Making"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c13e8c9",
   "metadata": {},
   "source": [
    "#### 🔗 LangChain Version (Gets messy)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5196b08f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define tools\n",
    "def check_order_status(order_id: str) -> str:\n",
    "    return f\"Order {order_id} is shipped\"\n",
    "\n",
    "def process_refund(order_id: str) -> str:\n",
    "    return f\"Refund initiated for {order_id}\"\n",
    "\n",
    "# Create agent - but controlling flow is harder\n",
    "llm = ChatOpenAI(model=\"gpt-4\")\n",
    "tools = [check_order_status, process_refund]\n",
    "\n",
    "# LangChain agents can use tools, but:\n",
    "# - Hard to add conditional logic (if X, do Y, else Z)\n",
    "# - Can't easily loop back to ask clarifying questions\n",
    "# - State management requires extra work"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c39966cd",
   "metadata": {},
   "source": [
    "#### 📈 LangGraph Version (Much better for this!)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1c05f94",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define tools (same as LangChain example)\n",
    "def process_refund(order_id: str) -> str:\n",
    "    return f\"Refund initiated for {order_id}\"\n",
    "\n",
    "class State(TypedDict):\n",
    "    messages: list[str]\n",
    "    user_intent: str\n",
    "    order_id: str\n",
    "    confidence: float\n",
    "    needs_clarification: bool\n",
    "    clarification_count: int  # Add counter to prevent infinite loops\n",
    "\n",
    "def classify_intent(state: State) -> State:\n",
    "    # Analyze user message\n",
    "    # ... LLM call to classify (would use state[\"messages\"][-1])\n",
    "    last_message = state[\"messages\"][-1] if state[\"messages\"] else \"\"\n",
    "\n",
    "    # Check if we already have an order_id (from clarification response)\n",
    "    if state.get(\"order_id\") and state[\"order_id\"]:\n",
    "        # If we have order_id, we're confident enough to proceed\n",
    "        state[\"user_intent\"] = \"refund\"\n",
    "        state[\"confidence\"] = 0.9\n",
    "    elif \"refund\" in last_message.lower():\n",
    "        state[\"user_intent\"] = \"refund\"\n",
    "        # Low confidence if no order_id provided\n",
    "        state[\"confidence\"] = 0.6 if not state.get(\"order_id\") else 0.9\n",
    "    else:\n",
    "        state[\"user_intent\"] = \"unknown\"\n",
    "        state[\"confidence\"] = 0.3\n",
    "\n",
    "    return state\n",
    "\n",
    "def should_clarify(state: State) -> Literal[\"clarify\", \"execute\"]:\n",
    "    # Conditional routing based on confidence\n",
    "    # Also check if we've asked too many times\n",
    "    if state.get(\"clarification_count\", 0) >= 2:\n",
    "        # After 2 clarifications, proceed anyway\n",
    "        return \"execute\"\n",
    "    if state[\"confidence\"] < 0.7:\n",
    "        return \"clarify\"\n",
    "    return \"execute\"\n",
    "\n",
    "def ask_clarification(state: State) -> State:\n",
    "    state[\"messages\"].append(\"Which order would you like to refund?\")\n",
    "    state[\"clarification_count\"] = state.get(\"clarification_count\", 0) + 1\n",
    "    # Simulate getting order_id from user response (in real app, this would come from user input)\n",
    "    # For demo purposes, set a default order_id after asking\n",
    "    if state[\"clarification_count\"] == 1:\n",
    "        # Simulate: user responds with order_id\n",
    "        state[\"order_id\"] = \"ORD-12345\"\n",
    "    return state\n",
    "\n",
    "def execute_action(state: State) -> State:\n",
    "    if state[\"user_intent\"] == \"refund\":\n",
    "        order_id = state.get(\"order_id\") or \"UNKNOWN\"\n",
    "        result = process_refund(order_id)\n",
    "        state[\"messages\"].append(result)\n",
    "    return state\n",
    "\n",
    "# Build the graph with conditional logic\n",
    "graph = StateGraph(State)\n",
    "\n",
    "graph.add_node(\"classify\", classify_intent)\n",
    "graph.add_node(\"clarify\", ask_clarification)\n",
    "graph.add_node(\"execute\", execute_action)\n",
    "\n",
    "graph.add_edge(START, \"classify\")\n",
    "graph.add_conditional_edges(\n",
    "    \"classify\",\n",
    "    should_clarify,  # Function decides next node\n",
    "    {\"clarify\": \"clarify\", \"execute\": \"execute\"}\n",
    ")\n",
    "graph.add_edge(\"clarify\", \"classify\")  # Loop back!\n",
    "graph.add_edge(\"execute\", END)\n",
    "\n",
    "app = graph.compile()\n",
    "\n",
    "# Execute with state persistence\n",
    "result = app.invoke({\n",
    "    \"messages\": [\"I want a refund\"],\n",
    "    \"user_intent\": \"\",\n",
    "    \"order_id\": \"\",\n",
    "    \"confidence\": 0.0,\n",
    "    \"needs_clarification\": False,\n",
    "    \"clarification_count\": 0\n",
    "})\n",
    "\n",
    "print(\"Final state:\")\n",
    "for msg in result[\"messages\"]:\n",
    "    print(f\"  - {msg}\")\n",
    "print(f\"\\nIntent: {result['user_intent']}\")\n",
    "print(f\"Order ID: {result.get('order_id', 'N/A')}\")\n",
    "print(f\"Confidence: {result['confidence']}\")\n",
    "\n",
    "# Generate and display the graph\n",
    "graph_image = app.get_graph().draw_mermaid_png()\n",
    "display(Image(graph_image))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "da3bd381",
   "metadata": {},
   "source": [
    "Why LangGraph?\n",
    "\n",
    "- The graph structure allows for loops and revisiting previous states, making it ideal for interactive systems Medium\n",
    "- Can pause and ask for clarification\n",
    "- Maintains conversation context across steps\n",
    "- Clear, debuggable flow"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a72c5b18",
   "metadata": {},
   "source": [
    "### 🔬 Example 3: Multi-Agent Research System"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3df42808",
   "metadata": {},
   "source": [
    "#### 📈 LangGraph Version (Perfect use case)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "91ad72b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ResearchState(TypedDict):\n",
    "    topic: str\n",
    "    web_results: list[str]\n",
    "    academic_papers: list[str]\n",
    "    summary: str\n",
    "    quality_score: float\n",
    "\n",
    "def web_researcher(state: ResearchState) -> ResearchState:\n",
    "    # Agent 1: Search the web\n",
    "    state[\"web_results\"] = [\"result1\", \"result2\"]\n",
    "    return state\n",
    "\n",
    "def academic_researcher(state: ResearchState) -> ResearchState:\n",
    "    # Agent 2: Search academic databases\n",
    "    state[\"academic_papers\"] = [\"paper1\", \"paper2\"]\n",
    "    return state\n",
    "\n",
    "def synthesizer(state: ResearchState) -> ResearchState:\n",
    "    # Agent 3: Combine findings\n",
    "    all_info = state[\"web_results\"] + state[\"academic_papers\"]\n",
    "    state[\"summary\"] = f\"Summary of {len(all_info)} sources\"\n",
    "    state[\"quality_score\"] = 0.85\n",
    "    return state\n",
    "\n",
    "def quality_check(state: ResearchState) -> str: # Routing function\n",
    "    # Decide if we need more research\n",
    "    if state[\"quality_score\"] < 0.8:\n",
    "        return \"web\"  # Loop back for more data\n",
    "    return \"done\"\n",
    "\n",
    "# Build multi-agent graph\n",
    "graph = StateGraph(ResearchState)\n",
    "\n",
    "# Add nodes\n",
    "graph.add_node(\"web\", web_researcher)\n",
    "graph.add_node(\"academic\", academic_researcher)\n",
    "graph.add_node(\"synthesize\", synthesizer)\n",
    "\n",
    "# Add simple edges\n",
    "graph.add_edge(START, \"web\")\n",
    "graph.add_edge(\"web\", \"academic\")\n",
    "graph.add_edge(\"academic\", \"synthesize\")\n",
    "\n",
    "# Add conditional edge (the loop!)\n",
    "graph.add_conditional_edges(\n",
    "    \"synthesize\",\n",
    "    quality_check,\n",
    "    {\"web\": \"web\", \"done\": END}\n",
    ")\n",
    "\n",
    "# Compile\n",
    "app = graph.compile()\n",
    "\n",
    "# Run it\n",
    "result = app.invoke({\n",
    "    \"topic\": \"AI Research\",\n",
    "    \"web_results\": [],\n",
    "    \"academic_papers\": [],\n",
    "    \"summary\": \"\",\n",
    "    \"quality_score\": 0.0\n",
    "})\n",
    "\n",
    "print(result)\n",
    "\n",
    "# Generate and display the graph\n",
    "graph_image = app.get_graph().draw_mermaid_png()\n",
    "display(Image(graph_image))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55101fd6",
   "metadata": {},
   "source": [
    "Why LangGraph? Multiple agents need to coordinate, share state, and potentially loop back for more information."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
